Skip to content

F# hot reload: Edit-and-Continue delta emission behind --test:HotReloadDeltas - #19941

Open
NatElkins wants to merge 385 commits into
dotnet:mainfrom
NatElkins:hot-reload-v2
Open

F# hot reload: Edit-and-Continue delta emission behind --test:HotReloadDeltas#19941
NatElkins wants to merge 385 commits into
dotnet:mainfrom
NatElkins:hot-reload-v2

Conversation

@NatElkins

@NatElkins NatElkins commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

This draft PR presents the complete F# hot reload implementation as a single branch against current main: dotnet watch (with a companion dotnet-watch change, linked below) patches running F# processes in place via the standard EnC pipeline (MetadataUpdater.ApplyUpdate) — the same runtime contract C# uses. Unsupported edits degrade to the rebuild-and-restart flow watch uses today, so a limited scope still behaves as a complete feature.

It is opened as a single draft per discussion with @T-Gro: one branch to review, build, and launch testing from. A decomposition into independently shippable PRs is laid out below and can begin whenever review reaches that stage. Earlier discussion: #11636.

Try it (30–45 min, two clones, copy-paste steps)

docs/hot-reload-quickstart.md walks from git clone to editing a running F# app — including adding a List.map (fun s -> s.ToUpper()) to a live method and watching it apply in place, state preserved. Short version: build this branch, build NatElkins/sdk fsharp-hotreload-watch-v2 (a complete .NET CLI with an F#-aware dotnet watch), copy the freshly built FSharp.Compiler.Service.dll into the SDK layout, add <OtherFlags>$(OtherFlags) --test:HotReloadDeltas</OtherFlags> to a console app, and dotnet watch run.

What works

  • Method body edits, including bodies containing closures, async, resume-point-stable task, and generics
  • Resumable state-machine shape edits (behind --test:HotReloadClassStateMachines): adding or removing a let!/do! in task and backgroundTask, which emits class-form (reference-type) state machines so the change is an AddInstanceFieldToExistingType plus a method update, matching C#. taskSeq and other resumable CEs share the same lowering path. Off by default; flag-off codegen is byte-identical.
  • Lambdas added / edited / removed across generations (new closure classes synthesized into the running process)
  • Member additions: methods, module functions and values, static/instance fields, properties, [<CLIEvent>] events
  • New type definitions: classes, records, unions, structs, modules, enums, interfaces, delegates, units of measure
  • Attribute add/change/remove on existing members; parameter renames
  • Multi-project: one watch session, per-project baselines, interleaved edits across an app and its referenced libraries
  • Line-shift edits (comment/whitespace above code) become pure sequence-point line updates — no delta, no restart
  • Rude edits (signature changes, captured-variable rename/type/scope changes, plus the F#-specific inline-annotation change) degrade to the standard rebuild-and-restart flow with a precise diagnostic. Adding or removing a mid-sequence let!/do! is rude under the default struct state machines, but becomes a supported edit (an AddInstanceFieldToExistingType plus a method update, matching C#) under --test:HotReloadClassStateMachines

Isolation and disabling

The feature is designed so that a compile without the flag is indistinguishable from main, and so that the whole feature can be turned off at one point if something goes wrong:

  • Off by default. Everything is behind --test:HotReloadDeltas (requires --debug+, incompatible with --optimize+), with class-form resumable state machines a further opt-in behind --test:HotReloadClassStateMachines. Both are off by default; no MSBuild property or SDK behavior changes in this repo.
  • Flag-off output is byte-identical to main, pinned by the EmittedIL suite (1212 baseline tests) and by dedicated determinism tests (DLL+PDB byte-equality across recompiles and across graph/sequential checking modes).
  • Flag-off cost is zero beyond cheap checks. A dedicated audit pass removed all unconditional work from the flag-off path: no reflection, no eager metadata snapshots, no per-name or per-closure side-channel probes, no extended lifetime of the optimized typed tree, and FSharpProjectSnapshot.fs is byte-identical to main (tracked-input staleness lives in the hot reload session layer instead).
  • One seam. The compiler driver integration is a single ICompilerEmitHook interface with a no-op default. Guard scripts under tests/scripts/ (run by the verification gate) enforce that only the intended files consume the hook and that the IlxGen name-generation path and fsi surface stay on their pinned shapes.
  • Disabling: drop the flag (compiler side); the companion dotnet-watch side additionally has a one-variable kill switch (DOTNET_WATCH_FSHARP_HOTRELOAD=0) that restores stock restart-on-edit behavior.

Architecture

  • Sessions are an explicit entity: FSharpChecker.CreateHotReloadSession returns a FSharpHotReloadSession holding per-project committed snapshots + emit baselines — the DebuggingSession/CommittedSolution shape from Roslyn, built on FSharpProjectSnapshot (the snapshot contract, not the experimental workspace surface), so it composes with the FSharpWorkspace direction without depending on it. Solution-wide commit/discard semantics, runtime-capability updates, active-statement intake.
  • A typed-tree semantic diff classifies every edit (Roslyn's SemanticEdit/RudeEdit model), gated on the runtime's advertised EnC capabilities (AddMethodToExistingType, NewTypeDefinition, GenericUpdateMethod, …). Anything unclassifiable fails closed.
  • Delta emission produces the standard EnC triplet (metadata/IL/PDB deltas with EncLog/EncMap), validated against recorded Roslyn EmitDifference reference deltas, with mdv, and against CoreCLR ApplyUpdate in runtime tests.
  • Closure identity is solved the way Roslyn solves it, adapted to F#'s lowering: lambdas get stable identity from a typed-tree occurrence model (ordinal chains + LCS alignment rather than syntax offsets), persisted in Roslyn's exact portable-PDB EnC CDI blob formats (the encoder round-trips Roslyn's own blobs byte-identically), with deterministic occurrence-derived closure-class names so any process can reconstruct identity from the PDB alone.

Design documentation (rendered, on this branch)

Doc What it covers
hot-reload-architecture.md Start here. The entity model: FSharpHotReloadSession, per-project committed snapshots + baselines on FSharpProjectSnapshot, the FSharpWorkspace relationship, determinism pins
hot-reload-closure-mapping.md The closure problem and its solution: lambda occurrence model, Roslyn-format EnC CDI PDB blobs, occurrence-derived deterministic closure naming, cross-process reconstruction; state-machine handling
hot-reload-member-additions.md Recorded Roslyn EmitDifference reference templates (EncLog/EncMap shapes per edit kind) and the F# emission matrix incl. every intentional fail-closed case
hot-reload-capabilities.md Runtime capability negotiation (Roslyn EditAndContinueCapabilities parity) and per-capability gating
hot-reload-active-statements.md The debugger-contract mirror: active statements, sequence-point updates, remapping; host wiring deferred

Scale and review shape

~118 commits, 159 files, ~60k insertions, of which ~34k are tests and ~2.4k docs. The src/ changes are predominantly new self-contained modules (IlxDeltaEmitter.fs, TypedTreeDiff.fs, HotReloadBaseline.fs, the AbstractIL EnC readers, the delta writer stack). Pre-existing files carry hook callouts plus one behavior-neutral refactor (ilwrite.fs MetadataTable record→class, exposing a baseline-row access seam for the delta writer); total deletions across the branch are 241 lines.

Proposed path to merging in pieces

The fail-closed design means scope can grow capability by capability — each unsupported case is already a rude edit with a diagnostic, so every intermediate state is a complete, working feature. The natural sequence:

  1. Behavior-neutral AbstractIL/ilwrite foundations (no feature, byte-identity evidence) — already staged separately as a 3-commit branch (refactor: AbstractIL EnC foundations (hot reload stack 1/n) NatElkins/fsharp#2)
  2. Session entity + typed-tree classification, with every edit classified rude — end-to-end complete at minimal scope (watch restarts on every edit, but through the proper pipeline). This is the PR where the architecture gets decided with reviewers.
  3. Method-body-only deltas (the core)
  4. Closure mapping + deterministic naming (the one chunk that touches IlxGen/name-generation paths — reviewed on its own)
  5. Member/field/type additions, generics, state machines — each already individually capability-gated
  6. Active statements / sequence-point updates

Evidence

Known limitations / future work

Companion PRs

Refresh status (2026-07-17)

  • Refreshed against dotnet/fsharp main at 5928e91; the current reviewed head is 9d2e6e3.
  • A dedicated review pass completed for this PR, its findings were fixed in the lowest owning slice, and the PR has no unresolved review threads.
  • The downstream session, in-process compiler, umbrella, and SDK branches were restacked after the fixes, so this slice remains part of the decomposed review train.
  • The complete compiler stack passed the 11-step hot reload verifier, 456 service tests, 243 component tests with 2 expected skips, and 1411 EmittedIL tests with 3 expected skips. Replacement CI passed on this exact head.

Refresh status (2026-07-22)

  • Current head: aaaf715, refreshed against dotnet/fsharp main 69fca7f.
  • Stack position: complete umbrella containing the reviewed wave-1 foundations, baseline reader, delta emitter, session/API, and experimental in-process slice. The focused PRs still target main and must be refreshed normally as earlier slices merge.
  • Every applicable July 21 automated finding is fixed in its lowest owning slice and propagated here. All review threads across the nine open F# PRs are resolved.
  • Exact-head local verifier: all 11 steps passed, including 468 service tests, 246 component passes with 2 documented manual-host skips, 29 metadata-parity tests, both smoke modes, and direct two-generation runtime apply. The flag-off EmittedIL gate passed 1411 tests with 3 known skips before the final flag-on-only corrections.
  • Exact-head Azure compiler CI passed all 46 jobs. The base-branch check_release_notes workflow still fails before analysis because pull_request_target refuses fork checkout.

Refresh status (2026-07-24)

  • Current head: 2c8d3a7081, with dotnet/fsharp main 1dc395ad34 and the exact refreshed focused stack merged in dependency order.
  • Every actionable review finding is fixed or explicitly answered, including the Windows loaded-file report and the added-await parity follow-up. Every review thread is resolved.
  • The full verifier passes on this exact head: solution build, all structural guards, 472 service tests, 248 component tests with 2 intentional manual-host skips, both smoke modes, and direct multi-delta runtime apply. Repository-wide Fantomas and diagnostic sorting checks pass.
  • Release notes are deduplicated and F# hot reload: Edit-and-Continue delta emission behind --test:HotReloadDeltas #19941 appears exactly once. Exact-head Azure build 1525313 passed all 46 jobs; the base-branch fork-checkout failure remains the documented Secure release-note checks for fork pull requests #20081 exception.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

✅ No release notes required

@NatElkins NatElkins mentioned this pull request Jun 12, 2026
@xperiandri

Copy link
Copy Markdown
Contributor

Looks like it requires rebase/conficts fix

Comment thread src/Compiler/Driver/fsc.fs Outdated
Comment thread src/Compiler/Generated/CompilerGeneratedNameMapState.fs Outdated
Comment thread src/Compiler/CodeGen/DeltaMetadataTables.fs Outdated
Comment thread src/Compiler/CodeGen/FSharpDeltaMetadataWriter.fs Outdated
Comment thread src/Compiler/TypedTree/CompilerGlobalState.fs
Comment thread src/Compiler/Generated/CompilerGeneratedNameMapState.fs Outdated
Comment thread src/Compiler/HotReload/ActiveStatements.fs
Comment thread src/Compiler/HotReload/HotReloadState.fs Outdated
Comment thread tests/FSharp.Compiler.Service.Tests/HotReload/ThreadSafetyTests.fs
Comment thread src/Compiler/TypedTree/TypedTreeDiff.fs
Comment thread src/Compiler/CodeGen/HotReloadPdb.fs
Comment thread src/Compiler/Service/service.fs Outdated
@WizMe-M

WizMe-M commented Jun 21, 2026

Copy link
Copy Markdown

Hi, I gave a try to hot-reload demo. I followed steps 1-5 and failed. What am I doing wrong?

First of all step 4 says:

Run it:
dotnet watch run --non-interactive
You should see ⌚ F# hot reload session prestarted, then the counter ticking once a second.
But my output was:

Missing `F# hot reload session prestarted`
HotReloadDemo> dotnet watch run --non-interactive
dotnet watch 🔥 Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.
dotnet watch 💡 Press Ctrl+R to restart.
Restore complete (0,4s)
    info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
  HotReloadDemo net11.0 succeeded (2,7s) → bin\Debug\net11.0\HotReloadDemo.dll

Build succeeded in 3,7s
dotnet watch ⌚ Loading projects ...
dotnet watch ⌚ Loaded 1 project(s) in 0,4s.
dotnet watch ⌚ Waiting for changes
hello (count: 1) <--------------------------- missing 'F# hot reload session prestarted'
Looks like SDK/F#-compiler with hot-reload wasn't built.

After that I've tried to modify Program.fs and got expected result (TLDR: not works):

TLDR: not works
HotReloadDemo> dotnet watch run --non-interactive
dotnet watch 🔥 Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.
dotnet watch 💡 Press Ctrl+R to restart.
Restore complete (0,4s)
    info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
  HotReloadDemo net11.0 succeeded (2,7s) → bin\Debug\net11.0\HotReloadDemo.dll

Build succeeded in 3,7s
dotnet watch ⌚ Loading projects ...
dotnet watch ⌚ Loaded 1 project(s) in 0,4s.
dotnet watch ⌚ Waiting for changes
hello (count: 1)
...
hello (count: 32)
dotnet watch ⌚ File updated: .\Program.fs
hello (count: 33)
...
hello (count: 56)
dotnet watch ⌚ File updated: .\Program.fs
hello (count: 57)
...
hello (count: 68)
dotnet watch 🔄 Restart requested. <------------------- ctrl+R
hello (count: 69)
hello (count: 70)
hello (count: 71)
dotnet watch ⌚ [HotReloadDemo (net11.0)] Exited
dotnet watch 🔄 Restarting.
Restore complete (0,5s)
    info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
  HotReloadDemo net11.0 succeeded (0,2s) → bin\Debug\net11.0\HotReloadDemo.dll

Build succeeded in 1,3s
dotnet watch ⌚ Loading projects ...
dotnet watch ⌚ Loaded 1 project(s) in 0,1s.
dotnet watch ⌚ Waiting for changes
HOT-RELOAD works (count: 1)   <------------------- only after ctrl+R
HOT-RELOAD works (count: 2)

Info:

  • OS: Windows 11 Pro (ran all steps from Powershell)
  • All $env variables were set correcly following manual
  • F#-compiler was built with no errors and warnings
  • SDK was built on third try with no errors and warnings
dotnet --info
HotReloadDemo> dotnet --info
.NET SDK:
 Version:           11.0.100-dev
 Commit:            ab1a0a0dbb
 Workload version:  11.0.100-manifests.844758e0
 MSBuild version:   18.8.0-preview-26277-111+6ca055abb

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.26200
 OS Platform: Windows
 RID:         win-x64
 Base Path:   ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\

.NET workloads installed:
 [maui-windows]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    11.0.0-preview.1.26102.3/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.maui\11.0.0-preview.1.26102.3\WorkloadManifest.json
   Install Type:        FileBased

 [maccatalyst]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    26.2.11310-net11-p1/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.maccatalyst\26.2.11310-net11-p1\WorkloadManifest.json
   Install Type:        FileBased

 [ios]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    26.2.11310-net11-p1/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.ios\26.2.11310-net11-p1\WorkloadManifest.json
   Install Type:        FileBased

 [android]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    36.1.99-preview.1.119/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.android\36.1.99-preview.1.119\WorkloadManifest.json
   Install Type:        FileBased

Configured to use workload sets when installing new manifests.
No workload sets are installed. Run "dotnet workload restore" to install a workload set.

Host:
  Version:      11.0.0-preview.6.26277.111
  Architecture: x64
  Commit:       6ca055abbe

.NET SDKs installed:
  11.0.100-dev [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 11.0.0-preview.5.26227.104 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 11.0.0-preview.6.26277.111 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.36 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.20 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.28 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 9.0.17 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 10.0.9 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 11.0.0-preview.5.26227.104 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 11.0.0-preview.6.26277.111 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 11.0.0-preview.5.26227.104 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 11.0.0-preview.6.26277.111 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR      [~/sdk-hotreload/artifacts/bin/redist/Debug/dotnet]
  DOTNET_MULTILEVEL_LOOKUP                 [0]
  DOTNET_ROLL_FORWARD_TO_PRERELEASE        [1]
  DOTNET_ROOT                              [~/sdk-hotreload/artifacts/bin/redist/Debug/dotnet]

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

@ijklam

ijklam commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

After spending one or two hours debugging this, I found why this cannot work on Windows. The issue is that on the Windows, when the program is running, the program file will be locked. As a result, the "dotnet build" executed by the dotnet-watch's FSharpHotReloadService fails, cause the hot reload cannot work.

I finally have managed to make it work, done some simple test on it, and found some problems:

  1. It seems working only when editing the content in the method body.
type Greeter() =
    let mutable count = 0

    member _.Message() =
        count <- count + 1  // Modifying this number is OK
        task {
           do! Task.Delay 2000 // Modifying this number does not take effect until restart
        }
        |> _.GetAwaiter().GetResult()
        sprintf "hello (count: %d)" count  // Modifying this string is OK

let greeter = Greeter()

while true do
    printfn "%s" (greeter.Message())   // Modifying this string doesn't take effect, until restart the program
    System.Threading.Thread.Sleep(1000)   // Changing this 1000 to 2000 doesn't take effect, until restart the program
  1. Simply adding a do! ... in a task block will need to restart the program.
    member _.Message() =
        count <- count + 1
        task {
          do! Task.Delay 2000
          do! Task.Delay 2000  // add this line
        }
  1. Adding or removing lines around a task needs restart program.
    member _.Message() =
        count <- count + 1  // remove this line
        task {
          do! Task.Delay 2000
        }

NatElkins added a commit to NatElkins/sdk that referenced this pull request Jun 22, 2026
The per-edit dotnet build refreshed the bin output the running process has
loaded. Windows locks that file against writes while the app runs, so the
build failed on every edit and the change fell back to a full restart instead
of applying in place.

Build the Compile target only (the obj intermediate assembly fsc writes) and
point the hot reload session's baseline and emit reads at that intermediate
assembly via FSharpProjectInfo.IntermediateAssemblyPath. The running process
never loads that file, so nothing is locked, and the bin output is left at
generation 0 until the next real restart. macOS and Linux already tolerated
overwriting a mapped file; this lines all three platforms up on the same path.

Reported on dotnet/fsharp#19941.
@NatElkins

NatElkins commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@ijklam got it right. The bridge ran a full dotnet build on every edit, and the last step of that build copies the assembly into bin, which is the file the running app has loaded. Windows keeps that locked while the process is alive, so the copy failed, the build failed with it, and the edit fell back to a restart. macOS and Linux let you replace a file that's mapped into a live process, so it only bit on Windows.

The fix builds just the Compile target instead, so fsc refreshes the intermediate assembly under obj and nothing writes to the loaded bin copy. The session reads the delta from that intermediate assembly now. bin stays put until the next real restart, and all three platforms take the same path.

It's on fsharp-hotreload-watch-v2 (fcf9b49). Only the dotnet-watch side changed, so you just need to pull that branch and rebuild the sdk clone, no need to rebuild the compiler or recopy the FCS dll. I don't have a Windows machine handy at the moment, so if one of you can confirm it works there now that would help a lot.

@WizMe-M the missing F# hot reload session prestarted line was a red herring. It only prints with DOTNET_WATCH_TRACE_FSHARP_HOTRELOAD=1 set, so not seeing it didn't mean anything was broken. I've reworded the quickstart to say that, and to point at the counter instead (it keeps climbing across edits when reloads land, and resets to 1 on a restart).

On the other observations: editing the top level while loop, or adding a do! to a task block, or moving lines around one, are mostly the expected limits rather than the Windows bug. The running loop is the active frame, so it can't be swapped while it's sitting on the stack, and inserting an await into a state machine is a rude edit in C# too. The count and string edits applying in place is exactly the case that should work. If the line-shift-around-a-task case still misbehaves after the rebuild, ping me, that one is worth a second look.

EDIT: Correcting myself on the struck-out line. @ijklam is right, C# hot reload does support adding an await/do! into a state machine, I read the EnC rules wrong. Roslyn treats it as a normal method update (gated on the runtime's AddInstanceFieldToExistingType capability), and only makes it a rude edit when the method is suspended at an active statement at the moment you save. So adding a do! to a task is a genuine parity gap on the F# side today, not an expected limit. I'm going to look into closing it.

@ijklam

ijklam commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

I think "adding or removing lines before a task block needs a restart" is something worth to be solved, if we hope the feature can help us in the routine coding, as there may be many task in a file, to make the program running asynchronously.

inserting an await into a state machine is a rude edit in C# too

By the way, C# hot reload supports this already.

@NatElkins NatElkins changed the title F# hot reload: Edit-and-Continue delta emission behind --enable:hotreloaddeltas F# hot reload: Edit-and-Continue delta emission behind --test:HotReloadDeltas Jun 23, 2026
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
… identity

The resumable-code/trait shape digest rendered builder-call type
instantiations through TType.ToString() (tyToString), whose depth-limited
LimitedToString(4) collapses a deeply-solved typar to the literal "True".
That made the digest non-injective: a `task` whose return type is `int`
both sides could render Bind<int,int,int> before an edit and
Bind<int,True,True> after, producing a false StateMachineShapeChange
(FSHRDL013) rude edit.

Render the digest through tryTypeIdentityFromTType -- the same injective
runtime-identity encoder the capture path already stores -- threading the
method's typar->ordinal map into collectLoweredShapeInfo and
traitConstraintShapeDigest, with a structured formatter and a display-string
fallback only for types the encoder cannot represent. Update the architecture
guard for the new traitConstraintShapeDigest signature.

Addresses review feedback on dotnet#19941.
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
FSharpEditAndContinueLanguageService.UpdateActiveStatements had no callers:
the live path is SetActiveStatements (FSharpHotReloadSession.SetActiveStatements
-> SetSessionActiveStatements -> editAndContinueService.SetActiveStatements).
Remove the dead member and its now-orphaned HotReloadSessionStore.UpdateActiveStatements
backing, whose only caller was the dead forwarder.

Addresses review feedback on dotnet#19941.
Comment thread src/Compiler/HotReload/EditAndContinueLanguageService.fs Outdated
Comment thread src/Compiler/HotReload/RudeEditDiagnostics.fs
Comment thread src/Compiler/HotReload/EditAndContinueLanguageService.fs Outdated
Comment thread src/Compiler/FSComp.txt
Comment thread src/Compiler/Driver/CompilerConfig.fs
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
…error channel

Rude-edit reasons were flattened to a single string at the point of failure
(UnsupportedEdit of string), discarding the per-edit Id, Severity and symbol
before they reached the public API and the dotnet-watch bridge. Carry them
structurally instead:

* RudeEditDiagnostic gains a Severity (FSharpDiagnosticSeverity). Every kind is
  Error today; severityOf is the single place to introduce a non-blocking
  Warning later (e.g. a "might not take effect" edit).
* HotReloadError.UnsupportedEdit and the public FSharpHotReloadError.UnsupportedEdit
  now carry a list of structured diagnostics. A new public FSharpHotReloadRudeEdit
  record exposes Id, Severity, Message and SymbolName.
* The active-statement, deleted-symbol, mapping-error and emit-exception paths
  wrap their ad-hoc reason via RudeEditDiagnostics.unsupported so every error
  still carries an id.

The id namespace is unchanged and still owned solely by
RudeEditDiagnostics.diagnosticId, so the channel itself is id-agnostic.

Addresses review feedback on dotnet#19941.
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
…he swap point

Document, at RudeEditDiagnostics.diagnosticId (the single place the rude-edit id
namespace is decided), why F# keeps its own FSHRDL* codes rather than emitting
Roslyn's ENC* codes, and note the closest ENC analogs. Kept as a separate commit
from the channel work so aligning with the ENC codes later is an isolated change.

Addresses review feedback on dotnet#19941.
NatElkins added a commit to NatElkins/sdk that referenced this pull request Jun 23, 2026
…t-watch

The F# bridge collapsed the rude-edit reason to a record/DU ToString() dump and
logged it at Debug only, so an edit that forces a restart gave the user no reason.
Now that FSharpHotReloadError.UnsupportedEdit carries a structured rude-edit list
(Id + Severity + Message), read it via reflection (TryFormatRudeEdits) into a clean
"{Id}: {Message}" reason and report it as a warning instead of at Debug. The
extraction is defensive: any shape mismatch falls back to ToString(), so the bridge
stays correct against older FCS builds (where UnsupportedEdit still carries a string).

Pairs with the FCS-side structured diagnostics channel on dotnet/fsharp#19941.
Addresses review feedback on #1.
Comment thread src/Compiler/HotReload/EditAndContinueLanguageService.fs
@NatElkins

Copy link
Copy Markdown
Contributor Author

@WizMe-M @ijklam I went back through this feedback while refreshing the stack.

The Windows file-lock problem is fixed in the SDK PR. The F# bridge now builds only the Compile target and reads the intermediate assembly under obj, so it no longer tries to overwrite the loaded bin assembly.

The await parity point is not being ignored either. F# task state machines are structs by default, so adding or removing a let! or do! changes their layout and still fails closed on the default path. This branch now has an experimental --test:HotReloadClassStateMachines path that emits reference-type state machines when explicitly enabled. Runtime ApplyUpdate tests cover adding and removing let!, adding one inside a loop, backgroundTask, and do!. That closes the Roslyn-style behavior behind the test flag, but I have not made it the default because changing the normal task state-machine representation needs a broader compatibility and performance decision.

The refreshed umbrella head is 3c49b38. The full local verifier passes at that head: 472 service tests, 248 component tests with 2 intentional manual-host skips, both smoke modes, and direct multi-delta runtime apply.

If the original Windows reproduction still fails with the refreshed SDK branch, please let me know what you see.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Build-Infra
Affects-Build-Infra: modifies eng/Build.ps1

Generated by PR Tooling Safety Check · opus46 8.2M ·

T-Gro added a commit that referenced this pull request Aug 5, 2026
* Avoid leaking a MeterListener per Cache in DEBUG builds (#19995)

* Avoid leaking a MeterListener per Cache in DEBUG builds

In DEBUG builds, every Cache instance created a CacheMetrics.CacheMetricsListener,
which starts a System.Diagnostics.Metrics.MeterListener registered in the
process-global metrics registry. These were never disposed, so they accumulated for
the lifetime of the process. Because every cache hit/miss/add publishes a measurement
to all registered listeners, the per-operation cost grew linearly with the number of
leaked listeners, so workloads that create many caches (for example repeated
ParseAndCheckProject / per-file checks) slowed down steadily.

Track the per-cache totals used by DebugDisplay directly, incrementing a small Stats
object alongside the existing global Meter counters, instead of via a per-cache
MeterListener. No listener is created, so nothing leaks, and DebugDisplay still works.
The now-unused CacheMetrics.Hit/Miss/Add/Update/Eviction/EvictionFail helpers are
replaced by a single recordMetric helper.

* Address review: drop per-cache CacheMetricsListener and cacheId tag

- Remove the CacheMetrics.CacheMetricsListener type. Its only per-cache use
  was the #if DEBUG debugListener each Cache created and never disposed, which
  was the leak this PR set out to fix. (majocha)
- Drop the per-instance cacheId tag (and nextCacheId). Measurements now carry
  only the cache name, shrinking the payload published to any connected
  exporter and removing the per-instance filtering that was cacheId's only
  purpose. (majocha)
- DebugDisplay and the cache tests read the existing name-aggregated stats via
  CacheMetrics.getTotalsByName / getRatioByName, populated by the single
  process-wide ListenToAll listener. No per-cache listener is created and no
  per-operation cost is added in any configuration, so there is no DEBUG-only
  overhead left to gate behind a separate directive. (T-Gro)
- Overload-cache tests enable ListenToAll and snapshot totals before/after to
  stay scoped to their own compilation; FSharpChecker
  .CreateOverloadCacheMetricsListener is removed.

* Update public SurfaceArea baseline after removing CacheMetricsListener

CacheMetricsListener was a public type, so dropping it changes the recorded
public surface. Remove its 10 entries from
FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl; the SurfaceArea test
now passes. Also note the single-listener assumption the cache metric tests
rely on.

* Apply fantomas formatting to Caches.fs

* Document why OverloadCacheTests is not parallelizable (global cache metrics state)

---------

Co-authored-by: Tomas Grosup <Tomas.Grosup@gmail.com>

* Bump FCSMinorVersion to 13 (keep main above 10.0.4xx servicing 43.12.400) (#20045)

* Update dependencies from https://github.com/dotnet/msbuild build 20260708.3 (#20048)

On relative base path root
Microsoft.Build , Microsoft.Build.Framework , Microsoft.Build.Tasks.Core , Microsoft.Build.Utilities.Core From Version 18.10.0-preview-26357-08 -> To Version 18.10.0-preview-26358-03

Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>

* Update dependencies from https://github.com/dotnet/roslyn build 20260708.9 (#20049)

On relative base path root
Microsoft.CodeAnalysis , Microsoft.CodeAnalysis.Compilers , Microsoft.CodeAnalysis.CSharp , Microsoft.CodeAnalysis.EditorFeatures , Microsoft.CodeAnalysis.EditorFeatures.Text , Microsoft.CodeAnalysis.ExternalAccess.FSharp , Microsoft.CodeAnalysis.Features , Microsoft.VisualStudio.LanguageServices From Version 5.10.0-1.26357.6 -> To Version 5.10.0-1.26358.9

Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>

* Add ResetCompilerGeneratedNameState to compiler-generated name generators (#20017)

Compiler-generated occurrence names (name@line-N) are allocated from process-wide
counters on CompilerGlobalState that accumulate across compilations. When a warm
checker re-emits the same project in-process, an unchanged closure therefore gets a
different occurrence suffix than the previous emit, so consumers that align generated
names across compilations (Edit-and-Continue delta emission, #19941)
cannot match them.

Add an internal ResetCompilerGeneratedNameState to NiceNameGenerator (clears the
per-(name, file) occurrence counters), StableNiceNameGenerator (clears the cached
stable names and the inner counters), and an aggregate on CompilerGlobalState that
resets all three generators, restoring the fresh-process name layout. Callers must
ensure no compilation is concurrently generating names.

No in-tree caller yet; the consumer is the hot reload emit path in #19941.
Covered by unit tests proving drift without reset, exact replay after reset, and that
the stable-name cache itself is cleared.

* Update dependencies from https://github.com/dotnet/msbuild build 20260709.10 (#20051)

On relative base path root
Microsoft.Build , Microsoft.Build.Framework , Microsoft.Build.Tasks.Core , Microsoft.Build.Utilities.Core From Version 18.10.0-preview-26358-03 -> To Version 18.10.0-1.26359.10

Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>

* [main] Update dependencies from dotnet/msbuild (#20055)

* Update dependencies from https://github.com/dotnet/msbuild build 20260710.4
On relative base path root
Microsoft.Build , Microsoft.Build.Framework , Microsoft.Build.Tasks.Core , Microsoft.Build.Utilities.Core From Version 18.10.0-1.26359.10 -> To Version 18.10.0-1.26360.4

* Update dependencies from https://github.com/dotnet/msbuild build 20260713.4
On relative base path root
Microsoft.Build , Microsoft.Build.Framework , Microsoft.Build.Tasks.Core , Microsoft.Build.Utilities.Core From Version 18.10.0-1.26359.10 -> To Version 18.10.0-1.26363.4

* Update dependencies from https://github.com/dotnet/msbuild build 20260714.11
On relative base path root
Microsoft.Build , Microsoft.Build.Framework , Microsoft.Build.Tasks.Core , Microsoft.Build.Utilities.Core From Version 18.10.0-1.26359.10 -> To Version 18.10.0-1.26364.11

* Update dependencies from https://github.com/dotnet/msbuild build 20260715.6
On relative base path root
Microsoft.Build , Microsoft.Build.Framework , Microsoft.Build.Tasks.Core , Microsoft.Build.Utilities.Core From Version 18.10.0-1.26359.10 -> To Version 18.10.0-1.26365.6

* Update dependencies from https://github.com/dotnet/msbuild build 20260716.8
On relative base path root
Microsoft.Build , Microsoft.Build.Framework , Microsoft.Build.Tasks.Core , Microsoft.Build.Utilities.Core From Version 18.10.0-1.26359.10 -> To Version 18.10.0-1.26366.8

* Update dependencies from https://github.com/dotnet/msbuild build 20260717.5
On relative base path root
Microsoft.Build , Microsoft.Build.Framework , Microsoft.Build.Tasks.Core , Microsoft.Build.Utilities.Core From Version 18.10.0-1.26359.10 -> To Version 18.10.0-1.26367.5

* Update dependencies from https://github.com/dotnet/msbuild build 20260719.1
On relative base path root
Microsoft.Build , Microsoft.Build.Framework , Microsoft.Build.Tasks.Core , Microsoft.Build.Utilities.Core From Version 18.10.0-1.26359.10 -> To Version 18.10.0-1.26369.1

* Update dependencies from https://github.com/dotnet/msbuild build 20260720.18
On relative base path root
Microsoft.Build , Microsoft.Build.Framework , Microsoft.Build.Tasks.Core , Microsoft.Build.Utilities.Core From Version 18.10.0-1.26359.10 -> To Version 18.10.0-1.26370.18

---------

Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>

* [main] Update dependencies from dotnet/roslyn (#20052)

* Update dependencies from https://github.com/dotnet/roslyn build 20260709.4
On relative base path root
Microsoft.CodeAnalysis , Microsoft.CodeAnalysis.Compilers , Microsoft.CodeAnalysis.CSharp , Microsoft.CodeAnalysis.EditorFeatures , Microsoft.CodeAnalysis.EditorFeatures.Text , Microsoft.CodeAnalysis.ExternalAccess.FSharp , Microsoft.CodeAnalysis.Features , Microsoft.VisualStudio.LanguageServices From Version 5.10.0-1.26358.9 -> To Version 5.10.0-1.26359.4

* Fix NU1605 package downgrades from Roslyn 5.10.0-1.26359.4 bump

The new Roslyn build adds a net472 dependency on Microsoft.VisualStudio.SDK
18.9.496-Preview and bumps its runtime deps to 10.0.8, causing package
downgrade errors:
- System.Collections.Immutable / System.Reflection.Metadata / System.Composition
  now required >= 10.0.8 (were pinned to 10.0.2)
- VS interops (OLE/Shell/TextManager.Interop) required >= 18.9.438
- Microsoft.VisualStudio.Threading required >= 18.7.19

The three interop packages are decoupled from the shared shell package
version since the VS SDK pins them newer than the other shell packages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix MSB3277 assembly conflicts from new Roslyn VS SDK 18.9 deps

The new Roslyn Microsoft.CodeAnalysis.ExternalAccess.FSharp (net472) now
depends on Microsoft.VisualStudio.SDK 18.9.496 and its coherent 18.9.x VS
package set, pulling newer transitive assemblies than fsharp's 18.0.x Shell
packages. This caused MSB3277 (assembly version conflicts) across the
vsintegration projects for:
- System.Diagnostics.DiagnosticSource (10.0.2 vs 10.0.8)
- Microsoft.VisualStudio.Validation (17.13 vs 18.7.1)
- StreamJsonRpc (2.23 vs 2.26.5)
- Microsoft.ServiceHub.Framework (4.9 vs 4.10.128)
- Microsoft.VisualStudio.RpcContracts (17.15.25 vs 18.9.453)

Bump DiagnosticSource to 10.0.8 (coherent with the other runtime deps) and
pin the four remaining transitive packages to the exact versions Roslyn
pulls, so all vsintegration projects resolve them coherently.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix runtime VS assembly load failures in legacy VS unit tests

The Roslyn 5.10.0-1.26359.4 bump pulls Microsoft.VisualStudio.SDK 18.9.496 which transitively upgrades the editor assemblies (Microsoft.VisualStudio.Text.*, .Editor) to 18.9.123 and Shell.15.0 to 18.9.x. Two runtime-only breaks remained after the earlier NU1605/MSB3277 build-time fixes, both surfacing as a ReflectionTypeLoadException in the VsMocks MEF catalog that failed all ~1959 legacy VS unit tests:

1. Microsoft.VisualStudio.Platform.VSEditor is not pulled transitively, so it stayed pinned at 18.0.404-preview and its implementation types no longer bind against the newer Text.Internal 18.9.123 interfaces. Pin VSEditor to 18.9.123 to match.

2. Shell.15.0 18.9.x references Microsoft.VisualStudio.SolutionPersistence at runtime without declaring it as a NuGet dependency; deploy it next to the VS unit-test host (scoped to test projects to keep it out of the VSIX).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix flaky AOT CI build: pass -ci to disable UpdateXlfOnBuild

The Build_And_Test_AOT_Windows job runs '.\Build.cmd -pack' without -ci, so ContinuousIntegrationBuild is not set. Arcade then enables UpdateXlfOnBuild, which flakily fails with 'MSB4057: The target UpdateXlf does not exist' on FSharp.Core (the classic_metadata leg failed while the identical compressed_metadata leg passed). Every other CI job builds via CIBuildNoPublish.cmd/cibuild.sh, which pass -ci. Add -ci here for consistency so ContinuousIntegrationBuild=true and UpdateXlfOnBuild stays disabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update dependencies from https://github.com/dotnet/roslyn build 20260709.5
On relative base path root
Microsoft.CodeAnalysis , Microsoft.CodeAnalysis.Compilers , Microsoft.CodeAnalysis.CSharp , Microsoft.CodeAnalysis.EditorFeatures , Microsoft.CodeAnalysis.EditorFeatures.Text , Microsoft.CodeAnalysis.ExternalAccess.FSharp , Microsoft.CodeAnalysis.Features , Microsoft.VisualStudio.LanguageServices From Version 5.10.0-1.26358.9 -> To Version 5.10.0-1.26359.5

* Update dependencies from https://github.com/dotnet/roslyn build 20260713.9
On relative base path root
Microsoft.CodeAnalysis , Microsoft.CodeAnalysis.Compilers , Microsoft.CodeAnalysis.CSharp , Microsoft.CodeAnalysis.EditorFeatures , Microsoft.CodeAnalysis.EditorFeatures.Text , Microsoft.CodeAnalysis.ExternalAccess.FSharp , Microsoft.CodeAnalysis.Features , Microsoft.VisualStudio.LanguageServices From Version 5.10.0-1.26358.9 -> To Version 5.10.0-1.26363.9

* Update dependencies from https://github.com/dotnet/roslyn build 20260714.9
On relative base path root
Microsoft.CodeAnalysis , Microsoft.CodeAnalysis.Compilers , Microsoft.CodeAnalysis.CSharp , Microsoft.CodeAnalysis.EditorFeatures , Microsoft.CodeAnalysis.EditorFeatures.Text , Microsoft.CodeAnalysis.ExternalAccess.FSharp , Microsoft.CodeAnalysis.Features , Microsoft.VisualStudio.LanguageServices From Version 5.10.0-1.26358.9 -> To Version 5.10.0-1.26364.9

* Update dependencies from https://github.com/dotnet/roslyn build 20260715.2
On relative base path root
Microsoft.CodeAnalysis , Microsoft.CodeAnalysis.Compilers , Microsoft.CodeAnalysis.CSharp , Microsoft.CodeAnalysis.EditorFeatures , Microsoft.CodeAnalysis.EditorFeatures.Text , Microsoft.CodeAnalysis.ExternalAccess.FSharp , Microsoft.CodeAnalysis.Features , Microsoft.VisualStudio.LanguageServices From Version 5.10.0-1.26358.9 -> To Version 5.10.0-1.26365.2

* Update dependencies from https://github.com/dotnet/roslyn build 20260715.3
On relative base path root
Microsoft.CodeAnalysis , Microsoft.CodeAnalysis.Compilers , Microsoft.CodeAnalysis.CSharp , Microsoft.CodeAnalysis.EditorFeatures , Microsoft.CodeAnalysis.EditorFeatures.Text , Microsoft.CodeAnalysis.ExternalAccess.FSharp , Microsoft.CodeAnalysis.Features , Microsoft.VisualStudio.LanguageServices From Version 5.10.0-1.26358.9 -> To Version 5.10.0-1.26365.3

---------

Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Localized file check-in by OneLocBuild Task: Build definition ID 499: Build ID 3013177 (#20023)

Co-authored-by: Copilot <copilot@github.com>

* [main] Update dependencies from dotnet/arcade (#20054)

* Update dependencies from https://github.com/dotnet/arcade build 20260708.3
On relative base path root
Microsoft.DotNet.Arcade.Sdk From Version 10.0.0-beta.26324.4 -> To Version 10.0.0-beta.26358.3

* Update dependencies from https://github.com/dotnet/arcade build 20260716.3
On relative base path root
Microsoft.DotNet.Arcade.Sdk From Version 10.0.0-beta.26324.4 -> To Version 10.0.0-beta.26366.3

* Update dependencies from https://github.com/dotnet/arcade build 20260717.6
On relative base path root
Microsoft.DotNet.Arcade.Sdk From Version 10.0.0-beta.26324.4 -> To Version 10.0.0-beta.26367.6

* Re-run CI (flaky infrastructure failures unrelated to Arcade bump)

The two failing jobs on this darc dependency PR were flaky/infra failures,
not caused by the Arcade SDK version bump:
- WindowsCompressedMetadata transparent_compiler_release: FSharp.Compiler.Service.Tests
  host hang hitting the 5m hangdump timeout (createdump MiniDumpWriteDump failure).
- IcedTasks_Test_Debug Regression Test: net9.0-only 'Entry point was not found'
  in the third-party FSharp.Control.TaskSeq DisposeAsync path (passed on net8.0/net10.0).
Both signatures recur on unrelated PRs (e.g. IcedTasks on #19941).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update dependencies from https://github.com/dotnet/arcade build 20260721.2
On relative base path root
Microsoft.DotNet.Arcade.Sdk From Version 10.0.0-beta.26324.4 -> To Version 10.0.0-beta.26371.2

---------

Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>
Co-authored-by: T-Gro <15220165+T-Gro@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Tests/source context: support multiple carets (#20077)

* Move VS language-service logic tests to FSharp.Compiler.Service.Tests (#20033)

* Move VS language-service logic tests to FSharp.Compiler.Service.Tests

Port completion, quick info, parameter info, go-to-definition, and
diagnostics coverage from the Windows-only VS Salsa suite to the
cross-platform FSharp.Compiler.Service.Tests. The legacy suite keeps only
the tests that genuinely exercise Visual Studio integration.

* Add Roslyn-format EnC CustomDebugInformation codec and portable PDB method CDI emission (#20018)

* Add Roslyn-format EnC CustomDebugInformation codec and portable PDB method CDI emission

Adds an internal AbstractIL module implementing, byte for byte, the three Portable PDB
CustomDebugInformation blob formats Roslyn persists per method for Edit and Continue
(EnC Local Slot Map, EnC Lambda and Closure Map, EnC State Machine State Map), with
serializers, deserializers, a portable PDB read-back helper, and an occurrence-key
packing helper for deterministic syntax-offset slots.

Plumbs an optional methodCustomDebugInfoRows side channel through the IL binary writer
options into the portable PDB generator so a compilation can attach CDI rows to named
methods. Names that do not identify exactly one method row are dropped. All existing
writer call sites pass an empty map, so emitted PDBs are byte-identical to before.

No in-tree caller populates the map yet; the consumer is the F# hot reload work in
#19941, following the same pattern as #20017 (land isolated, test-covered
infrastructure first, wire the feature later).

Tests: blob round-trips, Roslyn golden-byte encodings, cross-validation against
CDI blobs emitted by a real Roslyn compilation, fail-closed occurrence-key packing
(including an int32-overflow regression where a wrapped negative key previously
escaped the bound check), and end-to-end synthetic PDB emission proving correct
MethodDef parenting, zero rows for an empty map, and no rows for absent or
ambiguous names.

* Add stable synthesized-name replay infrastructure for hot reload (#20024)

* Extract stable synthesized-name replay layer

Add internal generated-name normalization and synthesized-name map replay support as a standalone slice. The new map state is side-channel based, all new compiler modules remain internal, and CompilerGlobalState preserves the existing no-map counter path while checking an accessor captured once per compiler state.

Route existing IlxGen generated-name allocations through inert helper wrappers, add pure name-map and normalizer tests, add a normal compilation determinism guard over emitted generated names, and document the extracted seams in P5_REPORT.md.

Verification: built FSharp.Compiler.Service, FSharp.Compiler.Service.Tests, FSharp.Compiler.ComponentTests, and FSharpSuite.Tests in Release; ran the migrated service test classes, the component determinism class, FSharpSuite DeterministicTests, and the FCS SurfaceArea class successfully.

* Fix generated-name scope test in stable names slice

* Validate hot reload generated names before classification

* Format hot reload compiler sources

Verified with the repository-wide Fantomas check.

* Retry CI after Linux runner memory exhaustion

* Make synthesized name snapshots deterministic

* Fix #19457: lift CE constructs from plain let RHS in computation expressions (#19868)

* Fix attribute resolution in recursive module/namespace scopes (#19744)

* Correct StructLayout size emission for data-less struct unions (#19759)

* Report FS3888 for generic attribute type abbreviations instead of FS0193 (#19915)

* Move to .NET 11 (SDK, Arcade, product TargetFramework) (#20080)

Upgrade the repo to build on .NET 11 and target net11.0, plus the
adaptations the SDK/Arcade 11 bump forces.

Core version switch:
- global.json: sdk.version 11.0.100-preview.6.26359.118 with
  rollForward=latestMinor + allowPrerelease (newer local 11.x still wins).
  A 2-part "11.0" is not a valid concrete SDK version, so the muxer fell
  back to $host$ and the end-to-end tests built with the machine net10 SDK
  (NETSDK1045); a concrete version resolves .dotnet's net11 SDK.
  Arcade.Sdk 11.0.0-beta.26369.1.
- eng/TargetFrameworks.props: FSharpNetCoreProductTargetFramework net11.0.
- eng/Version.Details.xml + eng/Version.Details.props: Arcade.Sdk
  11.0.0-beta.26369.1 (+Sha) — the value Maestro flows from dotnet/arcade
  onto the net11 channel, not a hand-picked one.
- eng/Versions.props: MicrosoftTestPlatformVersion 18.0.1 (net11 SDK bundles
  vstest 18.x; Microsoft.TestPlatform.ObjectModel must track that generation).
- eng/common: regenerated to Arcade 11 (26369.1).

Arcade-11 / SDK adaptations:
- Microsoft.FSharp.Compiler.fsproj: NuGetRepack property casing, drop the
  obsolete UsingTask, add no-op PackageReleasePackages override (#19557).
- fsi.fsproj: PublishReadyToRun=false (crossgen2 preview crashes on fsi).
- tests/Directory.Build.props: mark .ComponentTests IsTestProject (excludes
  from SymStore PDB conversion that crashes on large test assemblies).
- FSharp.DependencyManager.ProjectFile.fs: resolve framework-provided
  assemblies (Microsoft.Extensions.* now in the shared framework) for
  FSI #r "nuget:"; RestoreEnablePackagePruning=false.
- regression-test-jobs.yml: install the compiler SDK into the TestRepo.

net11 test-behavior:
- EditorTests.fs: RegexOptions.AnyNewLine (2048) under NET11_0_OR_GREATER.
- CompilerAssert.fs: derive runtimeconfig runtime version from
  FrameworkDescription + rollForward LatestMinor (preview is semver-lower).
- ILChecker.fs: normalize System.Linq assembly extern (version-independent).
- DependencyManagerInteractiveTests.fs: on net11 Microsoft.Extensions.* are
  shared-framework, so #r "nuget:" resolves the ref-pack path and one root.
- ilverify.ps1: map versioned netN.0 baselines to generic netcoreapp;
  rename the two FSharp.Compiler.Service baselines accordingly.
- EndToEndBuildTests: MicrosoftTestPlatformVersion 18.0.1.

Validated: ./build.sh -c Release green (0/0); EmittedIL 1413 pass/0 fail;
EditorTests AnyNewLine pass; DependencyManager nuget-roots test pass;
ilverify FCS net11.0 exact-matches baseline.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Support NotNullIfNotNullAttribute (#19977)

* Move SDL/TSA validation to 1ES templates after Arcade 11 upgrade (#20096)

Arcade 11 removed the SDL post-build scripts and the SDLValidationParameters parameter, breaking the official build. Move PoliCheck exclusions into the 1ES sdl: block and drop the obsolete post-build parameter and its variable group.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 7df99ba6-98b9-4cab-898b-422577b9e6dc

* Compiled ToStrings under -reflectionfree for DUs and Records (#19976)

* Add a compiler intrinsic for the 'string' operator

Adds string_operator_info / mkCallStringOperator so generated code can call
Operators.string. These lines are duplicated by the interpolated-string PR
(#19971); kept identical there so a future merge resolves cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Generate a match-based ToString for unions under --reflectionfree

Under --reflectionfree the union ToString previously emitted nothing, so
DUs fell back to Object.ToString() (the namespace-qualified type name).
Instead generate a match over the cases that builds "CaseName(f0, f1, ...)"
using the 'string' operator on each field, via a TypedTree expression fed
to CodeGenMethodForExpr. This recurses naturally into nested unions and is
reflection-free. The default (sprintf "%+A") path is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Extract mkStringConcat helper for arity-dispatched String.Concat

The "concatenate a list of string exprs, picking the cheapest String.Concat
overload by arity" pattern was duplicated in CheckExpressions (interpolation
lowering) and the optimizer, and our new union ToString used the array
overload unconditionally. Extract mkStringConcat into TypedTreeOps.ExprOps
and route all three through it. This also lets single-field union cases emit
Concat3 instead of allocating a string[] (IlxGen runs after the optimizer, so
nothing else would collapse that array form).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix generated union ToString for generic unions

The match-based ToString body is a TypedTree expression codegen'd via
CodeGenMethodForExpr, but it was built with `eenv`, which lacks the tycon's
type parameters. For generic unions this produced wrong IL: the wrong case
branch (always the null-as-true-value case) or a NullReferenceException for
single-case unions. Use `eenvinner` (the per-tycon environment) so the
generic method body resolves its type parameters. The old sprintf path was
unaffected because it emits raw IL off the pre-built ilThisTy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Render union ToString fields like option (null -> "null")

To make a generated union ToString consistent with how option/list format
their contents (LanguagePrimitives.anyToStringShowingNull), format each field
as: if (box field) is non-null then 'string field' else "null". Previously a
null field rendered as "" (the 'string' operator's null behaviour). Generated
inline rather than calling anyToStringShowingNull, which is internal to
FSharp.Core and so not callable from user-compiled code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Tidy reflection-free union ToString tests

Normalize union declarations to a leading '|', use System.Console.WriteLine
instead of printfn (the printf machinery is what these changes move away
from), and make the null-field test compare the union's rendering directly
against option's rather than asserting a fixed string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add reflection-free ToString to Result and Choice

Result and Choice had no ToString override, so they fell back to the
compiler-generated sprintf "%+A" one, which uses reflection. Give them
hand-written overrides mirroring option/list (String.Concat +
anyToStringShowingNull), e.g. Ok 5 -> "Ok(5)", Choice1Of2 7 -> "Choice1Of2(7)".
This is reflection-free / AOT-friendly and consistent with option's "Some(x)"
rendering. Note: this changes the observable ToString of Result/Choice from
the "%A"-style "Ok 5" to "Ok(5)".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Generate a single-line ToString for records under --reflectionfree

Records previously fell back to Object.ToString() (the namespace-qualified
type name) under --reflectionfree. Generate "{ F1 = v1; F2 = v2 }" on a single
line (no line breaks, unlike sprintf "%+A"), with fields formatted like union
fields (null -> "null", otherwise via 'string'). Factor the shared field
formatter and ToString-method emission out of the union path. The default
(sprintf "%+A") path is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update FSharp.Core surface-area baselines for Result/Choice ToString

Result and Choice`2..7 now declare an explicit ToString() override, so they
appear in the public surface area.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add release notes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Generate a single-line ToString for anonymous records under --reflectionfree

Drive anonymous-record ToString through the synthetic record tycon (already
built for equality/comparison) rather than sprintf "%A", so under
--reflectionfree it renders "{| Name = value; ... |}" on a single line.
GenRecordToStringMethod now takes open/close brace strings ("{ "/" }" for
records, "{| "/" |}" for anonymous records). The default (non-reflection-free)
codegen path is unchanged and still falls back to sprintf "%+A".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Test that a hand-written ToString override is kept under --reflectionfree

Addresses review feedback: generation is gated on `not (HasMember "ToString")`,
so a user-defined ToString on a union or record wins over the generated one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Rename ToString generators for clarity

Addresses review feedback: distinguish the reflective sprintf path from the
structural one. GenPrintingMethod -> GenSprintfPrintingMethod (the sprintf "%+A"
ToString/get_Message), GenToStringMethodFromExpr -> EmitToStringMethodDef.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Restore tabular layout for string_operator_info in TcGlobals

Addresses review feedback: keep the column-aligned layout of the surrounding
intrinsic table. Also makes these two lines byte-identical to the same intrinsic
added by #19971, so a future merge resolves cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add reflection-free ToString tests for field shapes, structs, anon records and recursion

Covers DU field shapes (multiple fields vs a single tuple field), explicit
vs unnamed field names rendering identically, struct unions/records,
anonymous and struct anonymous records, and finite recursive/nesting types.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add EmittedIL tests for reflection-free record and union ToString

Locks in the IL emitted under --reflectionfree: each field is boxed and
rendered through Operators.ToString with a null guard, and the parts are
joined with String.Concat (array form for the record, 3-arg form for the
single-field union case). Nullary union cases return the bare case name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Generate reflection-free ToString in the augmentation phase

The structural ToString for --reflectionfree records and unions was built in
IlxGen, after the optimizer, so its per-field 'string' operator calls were
never inlined: each value-type field was boxed and rendered through the
generic Operators.ToString, behind a null guard that is dead for a value type.

Move the generation into the type-augmentation phase (alongside
Equals/GetHashCode/CompareTo) so the body flows through the optimizer. The
'string' operator is now specialised - a value-type field renders via a direct,
allocation-free invariant-culture ToString with no boxing and no null guard
(reference fields keep the guard so null still renders as "null"). The shared
body builders live in AugmentTypeDefinitions; anonymous record types are
synthesized too late for augmentation, so they keep generating in IlxGen but
reuse the same builder.

Output is unchanged; the EmittedIL baselines are updated to the leaner IL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Guard generated reflection-free ToString against deep-recursion overflow

The augmentation-generated structural ToString recurses into fields, so a deeply
nested value can exhaust the stack with an uncatchable StackOverflowException.
Emit RuntimeHelpers.EnsureSufficientExecutionStack() at method entry (as C# records
do in PrintMembers) so it throws a catchable InsufficientExecutionStackException
instead, when the runtime provides the method. The guard is skipped for types whose
every field is a flat primitive (integer/float/decimal/string/char/bool/unit/enum),
which cannot recurse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Test the reflection-free ToString deep-recursion guard

A 1,000,000-deep value's generated ToString throws a catchable
InsufficientExecutionStackException rather than hard-crashing the process.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* revert ToString additions to fsharp.core types

* Remove stale FSharp.Core release note for the reverted Result/Choice ToString

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix code formatting in IlxGen.fs (dotnet fantomas)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* don't use quoted name

* int version of reflectionfree-printing doc

* doc tweaks

* Link release note to the printing doc and cover anonymous records

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test backticks

* Share the ToString recursion guard with anonymous records

The guard lived in MakeBindingsForToStringAugmentation, which anonymous
records bypass: they are synthesized too late for type augmentation and
reach mkRecdToString from IlxGen instead. Deep nesting overflowed the
stack rather than raising InsufficientExecutionStackException.

Move it into mkToStringRecursionGuard, applied inside mkRecdToString and
mkUnionToString, so every caller of the body builders gets it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add EmittedIL baselines for struct and anonymous record ToString

Struct records and unions read fields off the this pointer and switch on
the tag, and the anonymous record path is generated separately in IlxGen,
so each gets its own baseline.

The anonymous baseline omits the field reads: they name the anonymous
type, whose mangled name is not stable across compilations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix empty anonymous record ToString rendering a doubled space

The open/close braces carry inner spaces ("{| " and " |}"); with no
fields they abut and render "{|  |}". Trim the leading space when the
field list is empty, matching %A's "{| |}".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* tidy comment

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Run ilverify via the tool manifest instead of a hard-coded cache path (#20101)

* Record spreads (#18927)

* Implement interpolated strings via String.Concat (#19971)

* [main] Source code updates from dotnet/dotnet (#20058)

* Backflow from https://github.com/dotnet/dotnet / 50dbab4 build 322464

Diff: https://github.com/dotnet/dotnet/compare/920a0d55f8d87a0423dd3a89555f70d9c9004584..50dbab4de210e882172b07934e9666313b7065f1

From: dotnet/dotnet@920a0d5
To: dotnet/dotnet@50dbab4

[[ commit created by automation ]]

* Update dependencies from build 322464
Updated Dependencies:
Microsoft.Build, Microsoft.Build.Framework, Microsoft.Build.Tasks.Core, Microsoft.Build.Utilities.Core (Version 18.10.0-1.26359.10 -> 18.10.0-preview-26357-08)
[[ commit created by automation ]]

* Update dependencies from build 322734
No dependency updates to commit
[[ commit created by automation ]]

* Update dependencies from build 322911
No dependency updates to commit
[[ commit created by automation ]]

* Update dependencies from build 323048
No dependency updates to commit
[[ commit created by automation ]]

* Fix NU1903 audit failures from updated transitive dependencies

The codeflow update to Microsoft.Build.* now transitively pulls
System.Security.Cryptography.Xml 10.0.8 (newly flagged by GHSA advisories,
patched in 10.0.10) on .NET, and Microsoft.CodeAnalysis.Test.Resources.Proprietary
-> NETStandard.Library 1.6.1 pulls vulnerable System.Net.Http 4.3.0 and
System.Text.RegularExpressions 4.3.0 on net472.

- Bump System.Security.Cryptography.Xml override to 10.0.10 (Version.Details).
- Add .NET-only Cryptography.Xml overrides in fsc/fsi/FSharp.Build.UnitTests
  (net472 excluded: no such transitive there and its deps conflict with
  System.ValueTuple). These cascade to Microsoft.FSharp.Compiler and FSharpSuite.Tests.
- Override the net472 System.Net.Http/System.Text.RegularExpressions facades to
  patched 4.3.4/4.3.1 in FSharp.Test.Utilities.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Pin MessagePack to patched 2.5.302 to fix NU1902/NU1903 audit

StreamJsonRpc 2.25.29 pulls MessagePack transitively; some restore
environments resolve the vulnerable 2.5.198 (< 2.5.301 patched line),
tripping NuGetAudit warnings-as-errors in FSharp.Compiler.LanguageServer.Tests.
Add an explicit direct reference at 2.5.302 (StreamJsonRpc's own minimum,
already patched) so the resolved version is deterministic everywhere.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix malformed Version.Details.xml (duplicate closing Dependency tag)

A merge conflict resolution left a stray </Dependency> closing tag after
Microsoft.Build.Utilities.Core, making the XML invalid and failing the
Maestro Version.Details.props Validation and Codeflow verification checks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>

* Secure release-note checks for fork pull requests (#20081)

* Secure release-note checks for fork pull requests

* Address release-note workflow review feedback

* Update test project to net11 (#20104)

* Update test project to net11

Internal CI was failing since the move to net11 because restoring this
test project had to suddenly be done via network call to nuget.org

* Update target framework and PDB path in tests

* Update dependencies from https://github.com/dotnet/arcade build 20260803.2
On relative base path root
Microsoft.DotNet.Arcade.Sdk From Version 10.0.0-beta.26379.2 -> To Version 10.0.0-beta.26403.2

* Move to Roslyn's unified ExternalAccess library (#20099)

* LexFilter: drop non-strict mode (#20106)

* Enable Central Package Management with transitive pinning (#20084)

* Implement direct delegates (#19993)

* Fix check_release_notes 403 by restoring pull-requests: write and making the comment non-fatal (#20198)

check_release_notes runs via pull_request_target, so GitHub executes the workflow from the default branch (main). Creating the informational PR comment requires pull-requests: write, but #20081 reduced the token to read, turning the check red with HTTP 403 on any PR that had to create (not update) the comment - e.g. Maestro/darc PR #20133. Restore pull-requests: write so the comment posts, and guard the comment step with continue-on-error plus try/catch so posting can never fail the release-notes verdict. Supersedes #20200.

* Restore NuGetRepack UsingTask workaround for Arcade 10 in Microsoft.FSharp.Compiler.fsproj

The merge from main overwrote this project with main's Arcade-11 form, which
relies on the Microsoft.DotNet.NuGetRepack.Tasks package auto-importing the
UpdatePackageVersionTask UsingTask. On release/10.0.4xx (Arcade 10) that package
does not ship the build props, so the task must be declared explicitly. Without
it every packaging build leg failed with MSB4036 (task not found). Restores the
explicit UsingTask (tracked by #19557) while keeping the CPM-managed
PackageReference (no Version attribute).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update dependencies from https://github.com/dotnet/arcade build 20260804.3
On relative base path root
Microsoft.DotNet.Arcade.Sdk From Version 10.0.0-beta.26379.2 -> To Version 10.0.0-beta.26404.3

---------

Co-authored-by: Nat Elkins <nat@nelknet.com>
Co-authored-by: Tomas Grosup <Tomas.Grosup@gmail.com>
Co-authored-by: dotnet-maestro[bot] <42748379+dotnet-maestro[bot]@users.noreply.github.com>
Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: dotnet bot <dotnet-bot@dotnetfoundation.org>
Co-authored-by: T-Gro <15220165+T-Gro@users.noreply.github.com>
Co-authored-by: Eugene Auduchinok <eugene.auduchinok@jetbrains.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kerams <kerams@users.noreply.github.com>
Co-authored-by: Adam Boniecki <20281641+abonie@users.noreply.github.com>
Co-authored-by: Charles Roddie <charles.roddie@cantab.net>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Brian Rourke Boll <brianrourkeboll@users.noreply.github.com>
Co-authored-by: Charles Roddie <charles.roddie@summatic.co.uk>
Co-authored-by: Joey Robichaud <joseph.robichaud@microsoft.com>
Copilot-Session: 7df99ba6-98b9-4cab-898b-422577b9e6dc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

5 participants